查看原文
其他

用汇编语言编写一个病毒

维一零 计算机与网络安全 2022-06-01

信息安全公益宣传,信息安全知识启蒙。

加微信群回复公众号:微信群;QQ群:16004488

加微信群或QQ群可免费索取:学习教程



编写计算机病毒并不一定就是你所看到的关于破坏,还得要看你的病毒可以传播多广泛同时避免被检测,也得要比杀毒软件公司更为聪明。这事关创新和创造力。


一个计算机病毒在很多方面就像一个纸飞机。你需要使用聪明和具有创造性的方式去折飞机,并试图使它在不可避免的着陆前尽可能长久的飞翔。


在万维网之前,传播病毒是一种挑战。运气好的话,它会感染除了你自己之外的任何电脑。如果运气更好点,你的病毒将获得像鲸鱼病毒或米开朗基罗病毒一样的名声。


如果你想被视为一个“病毒作者”,你必须获得这类称号。在地下黑客组织里,在黑客/破解者/入侵者之中,我最尊重的是病毒作者。因为不是任何人都能做到,那是真的能够表现出他比别人拥有更深的、关于系统和软件方面的知识。


你不能指望简单地遵循常规就能成为一个病毒作者。


编写一个真正的病毒需要比一般“黑客”拥有更多的技能。多年以来,我没有成功的写出一个可以运行良好的二进制文件感染病毒。一直就是报错、报错、报错。这是一件令人沮丧的事情。


因此我坚持编写蠕虫、木马炸弹和ANSI炸弹。我坚持编写BBS的漏洞利用,也去逆向视频游戏软件以破解其版权保护。


每当我以为我的汇编技术终于足够,试图编写出一个病毒的时候,失败再次地落到我的脸上。我花了好几年的时间才能够编写出一个真正可运行的病毒。这就是为什么我着迷于病毒并且想找出一些真正的病毒作者。


在瑞安“elfmaster”奥尼尔传奇的书籍《学习Linux二进制程序分析》中,他指出:


这是一个超越常规编程约定的伟大挑战工程,它要求开发人员跳出传统模式,去操纵代码、数据和环境使其以某种方式表现,在与AV杀毒软件开发者的交流时,令我吃惊的是,他们旁边没有人有任何真正关于如何逆向一个病毒的想法,更不用说去设计什么真正的启发式来识别它们(除了签名)。事实上,病毒编写是非常困难的,并且需要标准比较严格的技能。


1. 使用汇编语言编写一个病毒

病毒是一种艺术。汇编和C(不使用代码库)将是你的画笔。今天,我将帮助你经历一些我面临过的挑战。让我们开始吧,看看你是否拥有成为一个艺术家的潜能!


与我之前的“源代码感染”病毒教程不同,这是更先进且具有挑战性的经历/运用(即使对经验丰富的开发人员)。但是,我鼓励你继续阅读并尽你所能地汲取。


让我们先描述一下我认为的、一个真正病毒应该有的特点


——病毒会感染二进制可执行文件

——病毒代码必须是独立的,它独立于其他文件、代码库、程序等

——被感染的宿主文件能够继续执行并且传播病毒

——病毒在不损害宿主文件的情况下表现得像一只寄生虫。受感染的宿主应继续像它被感染之前一样执行


因为我们要感染二进制可执行文件,所以简要列表介绍几个不同的可执行文件类型。


ELF-(可执行和链接的文件格式)Unix和类Unix系统标准的的二进制文件格式。这也被许多手机,游戏机(Playstation,任天堂)等等使用。


Mach-O-(Mach对象)被NeXTSTEP,macOS,iOS等等,所使用的二进制可执行文件格式,你其实在用它,因为所有的苹果手机都是这。


PE-(便携式可执行程序)用于32位和64位微软操作系统


MZ(DOS)- DOS支持的可执行文件格式…所有的微软32位及以下操作系统使用


COM(DOS)- DOS支持的可执行文件格式…所有的微系32位及以下操作系统使用

微软的病毒教程有许多,但是ELF病毒似乎更具挑战性并且教程稀缺,所以我将主要关注的是32位ELF程序的感染。


我将假设读者至少对病毒复制的方式有一个常规的理解。如果没有,我推荐你阅读我以前的博客文章主题:

https://cranklin.wordpress.com/2011/04/19/how-to-write-a-stupid-simple-computer-virus-in-3-lines-of-code/


https://cranklin.wordpress.com/2011/11/29/how-to-create-a-computer-virus/


https://cranklin.wordpress.com/2012/05/10/how-to-make-a-simple-computer-virus-with-python/

第一步是找到要感染的文件。DOS指令集可以方便寻找文件。AH:4Eh INT 21指令能够基于给定的文件描述找到第一个匹配的文件,而AH:4Fh INT 21指令可以找到下一个匹配的文件。


不幸的是,对于我们却不会这么简单。使用Linux汇编来检索文件列表,这相关的文档并不是很多。少数的几个回答中我们发现它依赖于POSIX系统的readdir()函数。但是我们是黑客,对么?让我们做黑客应该做的事情来实现它。


你应该熟悉的工具是strace。通过运行strace ls,我们看到了当运行ls命令时,跟踪到的系统调用和信号。


你感兴趣的调用是getdents。所以下一步是在http://syscalls.kernelgrok.com/查找”getdents”。这将给我们一个小小的提示,关于我们应该怎样使用它以及我们如何得到一个目录列表。下面就是我所发现的东西:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

      mov eax, 5      ; sys_open

    mov ebx, folder ; 目录名称

    mov ecx, 0

    mov edx, 0

    int 80h

    cmp eax, 0      ; 检测在eax中的fd是否 > 0 (ok) 

    jbe error       ; 不能打开文件,  以错误状态退出 

    mov ebx, eax    

    mov eax, 0xdc   ; sys_getdents64 

    mov ecx, buffer 

    mov edx, len 

    int 80h 

    mov eax, 6  ; 关闭

    int 80h

现在,我们指定的缓冲区里已经有了目录的内容,我们必须去解析它。出于某种原因,每个文件名的偏移量似乎并没有一致,但也可能是我错了。不过我只对那些原始的文件名字符串感兴趣。我所做的是打印缓冲区到标准输出,然后保存它到另一个文件,再使用十六进制编辑器来打开它。


我发现的规律是每个文件名都带有一个前缀,前缀由十六进制值0x00(null)后紧跟一个十六进制0x08构成。文件名是以null为终止的(后缀为一个十六进制0x00)。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

find_filename_start:

    ; 寻找在一个文件名开始前的序列0008

    add edi, 1

    cmp edi, len 

    jge done 

    cmp byte [buffer+edi], 0x00 

    jnz find_filename_start 

    add edi, 1

    cmp byte [buffer+edi], 0x08 

    jnz find_filename_start 

    xor ecx, ecx    ; 清空ecx,其将作为文件的偏移 

find_filename_end:

    ; 清空ecx,其将作为文件的偏移 

    add edi, 1 

    cmp edi, len    

    jge done

    mov bl, [buffer+edi]    ; 从缓冲区里移动文件名字节

    mov [file+ecx], bl 

    inc ecx                 ; 增加保存在ecx的偏移量

    cmp byte [buffer+edi], 0x00 ; 代表文件名的结尾

    jnz find_filename_end

    mov byte [file+ecx], 0x00 ; 到这我们就拿到文件名了,在其尾部添加一个0x00

    ;; 对该文件做一些操作 

    jmp find_filename_start ; 找下一个文件

其实有更好的方法来做这些事。你所需要做的只是去匹配目录条目结构的字节:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

struct linux_dirent {

               unsigned long  d_ino;     /* Inode number */

               unsigned long  d_off;      /* 下一个linux_dirent的偏移 */

               unsigned short d_reclen;  /* 这个linux_dirent的长度 */

               char           d_name[];  /* 文件名 (null结尾) */

                                 /* length is actually (d_reclen - 2 -

                                    offsetof(struct linux_dirent, d_name)) */

               /*

               char           pad;       // Zero padding byte

               char           d_type;    // File type (only since Linux

                                         // 2.6.4); offset is (d_reclen - 1)

               */

           }

struct linux_dirent64 {

               ino64_t        d_ino;    /* 64位inode number */

               off64_t        d_off;    /* 64位下个structure的偏移 */

               unsigned short d_reclen; /* 这个dirent的长度 */

               unsigned char  d_type;   /* 文件类型 */

               char           d_name[]; /*文件名 (null结尾) */

           };

但我正在使用的是我发现的一种模式,它没有使用到结构体中的偏移量。


下一步是检查文件,看看是否:

——这是一个ELF可执行文件

——它是不是已经被感染


早些时候,我介绍了一些关于不同操作系统使用的不同类型的可执行文件。这些文件类型在其文件头部都有不同的标志。例如,ELF文件总是从7f45 4c46开始。45-4c-46是ASCII字母E-L-F的十六进制表示。


如果你转储windows可执行文件十六进制数据,你会看到它开头是4D5A,代表字母M-Z。


十六进制转储OSX可执行文件显示了标记字节CEFA EDFE,也是小端的“FEED FACE”。


你可以在这里看到更多关于可执行文件格式和各自的标记:

https://en.wikipedia.org/wiki/List_of_file_signatures

在我的病毒中,我要把自己的标记写在了ELF文件头中第9 - 12字节里未使用的地方。这是一个不错的位置,可以用来存放一个双字“0edd1e00”——我的名字。 


我需要这个来标记我已经感染的文件,这样我就不会再次感染已经感染过的文件。不然受感染文件的长度将像雪球一样越滚越大,耶路撒冷病毒第一次就因此被检测到。


通过简单读取前12个字节,我们可以确定该文件是否是一个好的感染对象然后再继续下一个目标。我打算将每一个潜在的目标存储在一个单独的缓冲区,称之为“目标”。


现在它开始要变得困难了。为了感染ELF文件,你需要了解一切关于ELF文件结构的知识。这里是一个很好的学习起点:

http://www.skyfree.org/linux/references/ELF_Format.pdf

不同于简单的COM文件,ELF存在一些不同的挑战问题。简单来说,ELF文件包括:ELF头,程序头,节头,和指令操作码。


ELF头告诉我们关于程序头和节头的信息。它也告诉我们程序在内存中的入口点位置(首先执行的指令操作码)。


程序头告诉我们,哪个“段”属于TEXT段,哪个“段”属于DATA段,也给出其在文件中的偏移。


节头给出每个“节”和它们所属“段”的信息。这可能有点令人困惑。首先要明白的是一个可执行文件在磁盘上和它运行在内存中是不同的状态,而这些头给出了这两方面的相关信息。


TEXT段是可读取/执行的代码段,它包含了我们的代码和其他只读数据。


DATA段是可读/写的数据段,它包含了全局变量和动态链接的信息。


在TEXT段,有一个.text节和一个.rodata节。在DATA段中,有一个.data节和.bss节。

如果你熟悉汇编语言,这些节名应该对你来说听起来很熟悉。


.text是代码驻留的地方,.data是存储初始化全局变量的地方。.bss包含未初始化的全局变量,因为它是未初始化的,所以没有占用磁盘空间。


不像PE文件(微软的),ELF文件没有太多可以感染的区域。老式的DOS、COM文件几乎允许你在任何地方添加病毒代码,然后在100 h这个地址覆盖内存代码(因为COM文件总是在100 h的内存地址开始映射)。ELF文件不允许你写TEXT段。


下面这些是ELF感染病毒的主要方法:


2. 感染Text段填充区

感染.text节的尾部。我们可以利用ELF文件的特点,当其加载到内存中,尾部会被使用‘0’来填充成一个完整的内存页。受到内存页长度的限制,所以我们只能在32位系统上容纳一个4 kb病毒或在64位系统容纳2 mb病毒。这看起来可能很小,但也足够容纳用C或者汇编语言编写的小病毒。


这一目标的实现方法是:

——修改入口点(ELF头)到.text节的尾部

——增加节表(ELF头)里对应节的页长度

——增加Text段的文件长度和内存长度为病毒代码的长度

——遍历每个被病毒寄生后的程序头,根据页面长度增加对应的偏移

——找到Text段的最后一个节头,增加其节长度(在节头里)

——遍历每个被病毒感染后的节头,根据页面长度增加对应的偏移

——在.text节的尾部插入实际的病毒代码

——插入病毒代码后跳转到原始宿主的入口点执行


3. 反向感染Text段

在允许宿主代码保持相同虚拟地址的同时感染.text节区的前面部分。我们将反向扩展text段。在现代Linux系统中允许的最小虚拟映射地址是0x1000,这便是我们可以反向拓展text段的限制长度。


在64位系统上,默认的text段虚拟地址通常是0x400000,这就有可能给病毒留下减掉ELF头长度后的大小为0x3ff000的空间。在32位系统上,默认的text段虚拟地址通常是0x0804800,这就有可能产生更大的病毒。


这一目标的实现方式是:

——增加节表(在ELF头)里的偏移为病毒长度(对下一内存页对齐值取余)

——在Text段程序头里,根据病毒的长度(对下一内存页对齐值取余)减小虚拟地址(和物理地址)

——在Text段程序头里,根据病毒的长度(对下一内存页对齐值取余)增加文件长度和内存长度

——根据病毒的长度(再次取余),遍历每个程序头的偏移,增加它的值到大于text段 

——修改入口点(在ELF头)到原始的text段虚拟地址——病毒的长度(再次取余)

——根据病毒的长度(再次取余),增加程序头偏移(在ELF头)

——插入病毒实体到text段的开始位置


索取汇编语言学习教程请回复公众号:汇编


▼ 点击阅读原文,查看更多精彩文章。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存